﻿// The Nova Project by Ken Beckett.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php

using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;

using Nova.CodeDOM;

namespace Nova.UI
{
    /// <summary>
    /// The view model for a <see cref="CodeDOM.DocComment"/>.
    /// </summary>
    public class DocCommentVM : CommentBaseVM
    {
        #region /* STATICS */

        internal static void AddViewModelMapping()
        {
            CreateViewModel.Add(typeof(DocComment),
                delegate(CodeObject codeObject, bool isDescription, Dictionary<CodeObject, CodeObjectVM> dictionary) { return new DocCommentVM((DocComment)codeObject, dictionary); });
        }

        #endregion

        #region /* FIELDS */

        /// <summary>
        /// The content can be a simple string or a <see cref="ChildListVM{DocCommentVM}"/>, or a sub-tree of <see cref="CodeObjectVM"/>s.
        /// </summary>
        protected object _contentVM;

        #endregion

        #region /* CONSTRUCTORS */

        /// <summary>
        /// Create a view model instance for the specified <see cref="CodeDOM.DocComment"/>.
        /// </summary>
        public DocCommentVM(DocComment docComment, Dictionary<CodeObject, CodeObjectVM> dictionary)
            : base(docComment, dictionary)
        {
            object content = docComment.Content;
            if (content is string)
                _contentVM = content;
            else if (content is ChildList<DocComment>)
                _contentVM = CreateListVM<DocComment, DocCommentVM>((ChildList<DocComment>)content, dictionary);
            else if (content is CodeObject)
            {
                // Create BlockVMs specially so that the parent VM can be specified
                _contentVM = (content is Block ? new BlockVM((Block)content, this, dictionary) : CreateVM((CodeObject)content, false, dictionary));
            }
        }

        #endregion

        #region /* PROPERTIES */

        /// <summary>
        /// The underlying <see cref="CodeDOM.DocComment"/> model.
        /// </summary>
        public DocComment DocComment
        {
            get { return (DocComment)CodeObject; }
        }

        #endregion

        #region /* METHODS */

        /// <summary>
        /// Returns the <see cref="DocSummary"/> documentation comment, or null if none exists.
        /// </summary>
        public override DocSummaryVM GetDocSummary()
        {
            if (_contentVM is ChildListVM<DocCommentVM>)
                return Enumerable.FirstOrDefault(Enumerable.OfType<DocSummaryVM>(((ChildListVM<DocCommentVM>)_contentVM)));
            return null;
        }

        /// <summary>
        /// Get the root documentation comment object.
        /// </summary>
        public DocCommentVM GetRootDocComment()
        {
            DocCommentVM parent = this;
            while (parent.ParentVM is DocCommentVM)
                parent = (DocCommentVM)parent.ParentVM;
            return parent;
        }

        #endregion

        #region /* RENDERING */

        protected internal string GetContentForDisplay(RenderFlags flags)
        {
            // If NoTagNewLines is set, trim any leading AND/OR trailing whitespace from the content (any newlines
            // determine if the content starts and/or ends on the same line as the start/end tag, and NoTagNewLines
            // means we don't want to render them because we're not rendering the tags).
            string content = (string)_contentVM;
            if (flags.HasFlag(RenderFlags.NoTagNewLines))
                content = content.Trim();
            return content;
        }

        protected internal string GetContentForDisplay(DocTextVM docTextVM, bool isFirst, bool isLast, RenderFlags flags)
        {
            // If NoTagNewLines is set, trim any leading whitespace from the first child if it's a DocText, and trim
            // any trailing whitespace from the last child if it's a DocText.
            string text = docTextVM.DocText.Text;
            if (flags.HasFlag(RenderFlags.NoTagNewLines))
            {
                if (isFirst)
                    text = text.TrimStart();
                else if (isLast)
                    text = text.TrimEnd();
            }
            return text;
        }

        /// <summary>
        /// Determines if documentation comment text is rendered with a proportional font instead of a fixed font.
        /// </summary>
        public static bool UseProportionalFont = true;

        /// <summary>
        /// Use an alternate (non-XML) rendering for documentation comments if true.
        /// </summary>
        public static bool UseAlternativeFormat;

        protected virtual void RenderStart(CodeRenderer renderer, RenderFlags flags)
        {
            if (!flags.HasFlag(RenderFlags.Description) || DocComment.MissingEndTag)
            {
                string tagName = DocComment.TagName;
                if (tagName != null)
                {
                    if (!UseAlternativeFormat)
                        renderer.RenderText("<" + tagName + (_contentVM == null && !DocComment.MissingEndTag ? "/>" : ">"), DocComment.MissingEndTag ? WARNING_BRUSH : COMMENT_TAG_BRUSH, this);
                    else
                    {
                        string prefix = char.ToUpper(tagName[0]) + tagName.Substring(1) + (_contentVM != null ? " :  " : "");
                        renderer.RenderText(prefix, COMMENT_TAG_BRUSH, (UseProportionalFont ? TextStyle.Proportional : TextStyle.Normal), this);
                    }
                }
            }
        }

        protected virtual void RenderContent(CodeRenderer renderer, TextStyle textStyle, RenderFlags flags)
        {
            if (_contentVM is string)
                DocTextVM.RenderText(renderer, GetContentForDisplay(flags | (UseAlternativeFormat ? RenderFlags.NoTagNewLines : 0)), textStyle | TextStyle.NoCache, flags, this);
            else if (_contentVM is ChildListVM<DocCommentVM>)
            {
                bool lastWasTerm = false;
                ChildListVM<DocCommentVM> docComments = (ChildListVM<DocCommentVM>)_contentVM;
                for (int i = 0; i < docComments.Count; ++i)
                {
                    DocCommentVM docComment = docComments[i];
                    if (docComment is DocTextVM)
                    {
                        // Skip any DocText formatting after DocTerm tags when in alt render mode
                        if (!(UseAlternativeFormat && lastWasTerm))
                        {
                            string text = GetContentForDisplay((DocTextVM)docComment, i == 0, i == docComments.Count - 1, flags | (UseAlternativeFormat ? RenderFlags.NoTagNewLines : 0));
                            DocTextVM.RenderText(renderer, text, textStyle | TextStyle.NoCache, flags, this);
                        }
                    }
                    else
                    {
                        docComment.Render(renderer, textStyle, flags);
                        lastWasTerm = (docComment is DocListTermVM);
                    }
                }
            }
            else if (_contentVM is CodeObjectVM)
                ((CodeObjectVM)_contentVM).Render(renderer, flags);
        }

        protected virtual void RenderEnd(CodeRenderer renderer, RenderFlags flags)
        {
            if (!DocComment.MissingEndTag && (_contentVM != null || DocComment.MissingStartTag) && !flags.HasFlag(RenderFlags.Description) && !UseAlternativeFormat)
            {
                string tagName = DocComment.TagName;
                if (tagName != null)
                    renderer.RenderText("</" + tagName + ">", DocComment.MissingStartTag ? WARNING_BRUSH : COMMENT_TAG_BRUSH, this);
            }
        }

        public override void Render(CodeRenderer renderer, RenderFlags flags)
        {
            Render(renderer, TextStyle.Normal, flags);
        }

        public virtual void Render(CodeRenderer renderer, TextStyle textStyle, RenderFlags flags)
        {
            if (HideAll) return;

            bool isPrefix = flags.HasFlag(RenderFlags.IsPrefix);
            int newLines = DocComment.NewLines;
            bool isTopLevelDocComment = !flags.HasFlag(RenderFlags.InDocComment);
            if (isTopLevelDocComment)
            {
                if (!isPrefix && newLines > 0 && !flags.HasFlag(RenderFlags.SuppressNewLine))
                    renderer.NewLines(newLines, ParentVM);

                if (!flags.HasFlag(RenderFlags.NoBorder))
                    CreateBorder(renderer, flags);
                RenderDocNewLines(renderer, 0, flags, this);
            }
            else if (!isPrefix && newLines > 0)
                RenderDocNewLines(renderer, newLines, flags, this);

            RenderFlags passFlags = (flags & RenderFlags.PassMask) | RenderFlags.InDocComment;
            RenderStart(renderer, passFlags);
            RenderContent(renderer, textStyle, passFlags);
            RenderEnd(renderer, passFlags);

            if (isTopLevelDocComment)
            {
                if (!flags.HasFlag(RenderFlags.NoBorder))
                    renderer.ReturnToBorderParent(this);

                if (isPrefix)
                {
                    // If this object is rendered as a child prefix object of another, then any whitespace is
                    // rendered here *after* the object instead of before it.
                    // A documentation comment must always be followed by a newline if it's a prefix.
                    renderer.NewLines(newLines < 1 ? 1 : newLines, ParentVM);
                }
            }
        }

        public static void RenderDocNewLines(CodeRenderer renderer, int count, RenderFlags flags, DocCommentVM tag)
        {
            // Render one or more newlines (0 means a prefix without a newline)
            do
            {
                if (count > 0)
                    renderer.NewLine();
                if (!HideDelimiters && !flags.HasFlag(RenderFlags.Description))
                    renderer.RenderText(DocComment.ParseToken + " ", COMMENT_TAG_BRUSH, tag.GetRootDocComment());
                --count;
            }
            while (count > 0);
        }

        public override void RenderVisible(CodeRenderer renderer, RenderFlags flags)
        {
            flags |= RenderFlags.InDocComment;
            if (_contentVM is ChildListVM<DocCommentVM>)
                renderer.RenderVisibleList((ChildListVM<DocCommentVM>)_contentVM, flags);
            else if (_contentVM is CodeObjectVM)
                ((CodeObjectVM)_contentVM).RenderVisible(renderer, flags);
        }

        public override void UnRender()
        {
            if (_contentVM is ChildListVM<DocCommentVM>)
                ChildListHelpers.UnRender((ChildListVM<DocCommentVM>)_contentVM);
            else if (_contentVM is CodeObjectVM)
                ((CodeObjectVM)_contentVM).UnRender();
            base.UnRender();
        }

        #endregion

        #region /* COMMANDS */

        public static readonly RoutedCommand UseProportionalFontCommand = new RoutedCommand("Use Proportional Font for Doc Comments", typeof(DocCommentVM));
        public static readonly RoutedCommand AlternativeFormatCommand = new RoutedCommand("Use Alternative Format for Doc Comments", typeof(DocCommentVM));

        public static new void BindCommands(Window window)
        {
            WPFUtil.AddCommandBinding(window, UseProportionalFontCommand, proportionalDocComment_Executed);
            WPFUtil.AddCommandBinding(window, AlternativeFormatCommand, altDocComment_Executed);
        }

        private static void proportionalDocComment_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            UseProportionalFont = !UseProportionalFont;
            CodeRenderer.ReRenderAll();
        }

        private static void altDocComment_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            UseAlternativeFormat = !UseAlternativeFormat;
            CodeRenderer.ReRenderAll();
        }

        #endregion
    }
}
